package com.xinggq.auth.service.service.impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xinggq.auth.api.dto.login.AuthSafeUserInfoDTO;
import com.xinggq.auth.api.enums.SessionManagementStrategy;
import com.xinggq.auth.api.exception.AuthException;
import com.xinggq.auth.service.config.AuthProperties;
import com.xinggq.auth.service.service.TokenManagerService;
import com.xinggq.user.api.dto.UserInfoDTO;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * @Description
 * @Author xinggq
 * @Date 2025/10/23
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TokenManagerServiceImpl implements TokenManagerService {

  private final AuthProperties authProperties;
  private final RedisTemplate<String, String> redisTemplate;
  private final ObjectMapper objectMapper;

  /**
   * 令牌黑名单前缀
   */
  private static final String TOKEN_BLACKLIST_PREFIX = "auth:blacklist:";

  /**
   * 生成访问令牌
   *
   * @param userInfo 用户信息
   * @param rememberMe 是否记住我
   * @return 访问令牌
   */
  @Override
  public String generateAccessToken(UserInfoDTO userInfo, boolean rememberMe) {
    long validitySeconds = rememberMe ?
        authProperties.getTokenValiditySeconds() * 2 :
        authProperties.getTokenValiditySeconds();

    String token = UUID.randomUUID().toString();
    String tokenKey = authProperties.getTokenPrefix() + token;

    try {
      // 将用户信息序列化为JSON存储
      String userInfoJson = objectMapper.writeValueAsString(userInfo);
      redisTemplate.opsForValue().set(tokenKey, userInfoJson, validitySeconds, TimeUnit.SECONDS);
      return token;
    } catch (Exception e) {
      log.error("生成访问令牌失败", e);
      throw AuthException.operationFailed("生成访问令牌失败");
    }
  }

  /**
   * 生成刷新令牌
   *
   * @param userInfo 用户信息
   * @return 刷新令牌
   */
  @Override
  public String generateRefreshToken(UserInfoDTO userInfo) {
    String refreshToken = UUID.randomUUID().toString();
    String refreshTokenKey = authProperties.getRefreshTokenPrefix() + refreshToken;

    // 存储用户ID与刷新令牌的映射
    redisTemplate.opsForValue().set(
        refreshTokenKey,
        userInfo.getId().toString(),
        authProperties.getRefreshTokenValiditySeconds(),
        TimeUnit.SECONDS
    );

    return refreshToken;
  }

  /**
   * 验证令牌并获取用户信息
   *
   * @param token 令牌
   * @return 用户信息
   * @throws AuthException 令牌无效时抛出
   */
  @Override
  public AuthSafeUserInfoDTO validateTokenAndGetUserInfo(String token) {
    try {
      // 检查令牌是否在黑名单中
      if (isTokenBlacklisted(token)) {
        throw AuthException.invalidToken("令牌已被撤销");
      }

      String tokenKey = authProperties.getTokenPrefix() + token;
      String userInfoJson = redisTemplate.opsForValue().get(tokenKey);

      if (!StringUtils.hasText(userInfoJson)) {
        throw AuthException.invalidToken("令牌已过期或无效");
      }

      // 将JSON反序列化为用户信息
      UserInfoDTO userInfoDTO = objectMapper.readValue(userInfoJson, UserInfoDTO.class);
      return AuthSafeUserInfoDTO.builder()
          .userId(userInfoDTO.getId())
          .username(userInfoDTO.getUsername())
          .tenantId(userInfoDTO.getTenantId())
          .lastLoginTime(userInfoDTO.getLastLoginTime())
          .build();

    } catch (AuthException e) {
      throw e;
    } catch (Exception e) {
      log.error("验证令牌失败", e);
      throw AuthException.invalidToken("令牌验证失败");
    }
  }

  /**
   * 检查令牌是否在黑名单中
   *
   * @param token 令牌
   * @return 是否在黑名单中
   */
  private boolean isTokenBlacklisted(String token) {
    String blacklistKey = TOKEN_BLACKLIST_PREFIX + token;
    return redisTemplate.hasKey(blacklistKey);
  }


  /**
   * 验证刷新令牌并返回用户id
   *
   * @param refreshToken 刷新令牌
   * @return 用户ID
   * @throws AuthException 令牌无效时抛出
   */
  @Override
  public Long validateRefreshToken(String refreshToken) {
    try {
      String refreshTokenKey = authProperties.getRefreshTokenPrefix() + refreshToken;
      String userId = redisTemplate.opsForValue().get(refreshTokenKey);

      if (!StringUtils.hasText(userId)) {
        throw AuthException.invalidToken("刷新令牌已过期或无效");
      }

      return Long.parseLong(userId);
    } catch (Exception e) {
      log.error("刷新令牌验证失败", e);
      throw AuthException.invalidToken("无效的刷新令牌");
    }
  }

  /**
   * 撤销访问令牌
   *
   * @param token 要撤销的令牌
   */
  @Override
  public void revokeToken(String token) {
    try {
      // 从Redis中获取令牌信息
      String tokenKey = authProperties.getTokenPrefix() + token;
      String userInfoJson = redisTemplate.opsForValue().get(tokenKey);

      if (StringUtils.hasText(userInfoJson)) {
        // 删除令牌
        redisTemplate.delete(tokenKey);

        // 将令牌加入黑名单，防止在缓存过期前被重用
        String blacklistKey = TOKEN_BLACKLIST_PREFIX + token;
        redisTemplate.opsForValue().set(blacklistKey, "revoked",
            authProperties.getTokenValiditySeconds(), TimeUnit.SECONDS);
      }
    } catch (Exception e) {
      log.error("撤销令牌失败", e);
      throw AuthException.operationFailed("撤销令牌失败");
    }
  }

  /**
   * 撤销刷新令牌
   *
   * @param refreshToken 要撤销的刷新令牌
   */
  @Override
  public void revokeRefreshToken(String refreshToken) {
    try {
      String refreshTokenKey = authProperties.getRefreshTokenPrefix() + refreshToken;
      redisTemplate.delete(refreshTokenKey);
    } catch (Exception e) {
      log.error("撤销刷新令牌失败", e);
      throw AuthException.operationFailed("撤销刷新令牌失败");
    }
  }

  /**
   * 获取令牌过期时间
   *
   * @param token 令牌
   * @return 过期时间
   */
  @Override
  public Date getExpirationDateFromToken(String token) {
    try {
      String tokenKey = authProperties.getTokenPrefix() + token;
      long expireTime = redisTemplate.getExpire(tokenKey, TimeUnit.SECONDS);

      if (expireTime <= 0) {
        throw AuthException.invalidToken("令牌已过期或无效");
      }

      // 计算过期时间
      return new Date(System.currentTimeMillis() + (expireTime * 1000));
    } catch (Exception e) {
      log.error("获取令牌过期时间失败", e);
      throw AuthException.invalidToken("无效的令牌");
    }
  }

  /**
   * 获取访问令牌有效期
   *
   * @return 有效期（秒）
   */
  @Override
  public long getAccessTokenValidity() {
    return authProperties.getTokenValiditySeconds();
  }

  /**
   * 检查用户是否已有活跃会话
   *
   * @param userId 用户ID
   * @return 是否有活跃会话
   */
  @Override
  public boolean hasActiveSession(Long userId) {
    String userSessionKey = authProperties.getUserSessionPrefix() + userId;
    Set<String> sessions = redisTemplate.opsForSet().members(userSessionKey);
    return sessions != null && !sessions.isEmpty();
  }

  /**
   * 获取用户当前活跃会话数量
   *
   * @param userId 用户ID
   * @return 活跃会话数量
   */
  @Override
  public int getActiveSessionCount(Long userId) {
    String userSessionKey = authProperties.getUserSessionPrefix() + userId;
    Long count = redisTemplate.opsForSet().size(userSessionKey);
    return count != null ? count.intValue() : 0;
  }

  /**
   * 处理用户会话管理
   * 根据配置的会话管理策略处理重复登录
   *
   * @param userId 用户ID
   * @param newToken 新生成的Token
   * @return 是否允许登录
   */
  @Override
  public boolean handleUserSession(Long userId, String newToken) {
    SessionManagementStrategy strategy = SessionManagementStrategy.fromCode(
        authProperties.getSessionManagementStrategy());
    log.info("当前会话管理策略为：code:{},desc:{}", strategy.getCode(), strategy.getDescription());
    String userSessionKey = authProperties.getUserSessionPrefix() + userId;
    
    switch (strategy) {
      case ALLOW_MULTIPLE:
        // 允许多端登录，直接添加新会话
        redisTemplate.opsForSet().add(userSessionKey, newToken);
        redisTemplate.expire(userSessionKey, authProperties.getTokenValiditySeconds(), TimeUnit.SECONDS);
        log.info("用户 {} 允许多端登录，已添加新会话", userId);
        return true;
        
      case SINGLE_SESSION_KICK_OLD:
        // 单点登录 - 踢出旧会话
        revokeAllUserSessions(userId);
        redisTemplate.opsForSet().add(userSessionKey, newToken);
        redisTemplate.expire(userSessionKey, authProperties.getTokenValiditySeconds(), TimeUnit.SECONDS);
        log.info("用户 {} 单点登录，已踢出所有旧会话", userId);
        return true;
        
      case SINGLE_SESSION_REJECT_NEW:
        // 单点登录 - 拒绝新登录
        if (hasActiveSession(userId)) {
          log.warn("用户 {} 已有活跃会话，拒绝新登录", userId);
          return false;
        }
        redisTemplate.opsForSet().add(userSessionKey, newToken);
        redisTemplate.expire(userSessionKey, authProperties.getTokenValiditySeconds(), TimeUnit.SECONDS);
        return true;
        
      case LIMITED_CONCURRENT:
        // 限制并发会话数
        int currentCount = getActiveSessionCount(userId);
        if (currentCount >= authProperties.getMaxConcurrentSessions()) {
          // 踢出最旧的会话，保留最新的N-1个
          revokeOldUserSessions(userId, authProperties.getMaxConcurrentSessions() - 1);
        }
        redisTemplate.opsForSet().add(userSessionKey, newToken);
        redisTemplate.expire(userSessionKey, authProperties.getTokenValiditySeconds(), TimeUnit.SECONDS);
        log.info("用户 {} 限制并发登录，当前会话数: {}", userId, getActiveSessionCount(userId));
        return true;
        
      default:
        log.warn("未知的会话管理策略: {}", strategy);
        return true;
    }
  }

  /**
   * 撤销用户的所有会话
   *
   * @param userId 用户ID
   */
  @Override
  public void revokeAllUserSessions(Long userId) {
    String userSessionKey = authProperties.getUserSessionPrefix() + userId;
    Set<String> sessions = redisTemplate.opsForSet().members(userSessionKey);
    
    if (sessions != null && !sessions.isEmpty()) {
      // 将所有Token加入黑名单
      for (String token : sessions) {
        revokeToken(token);
      }
      // 清空用户会话集合
      redisTemplate.delete(userSessionKey);
      log.info("已撤销用户 {} 的所有会话，共 {} 个", userId, sessions.size());
    }
  }

  /**
   * 撤销用户的旧会话（保留最新的N个）
   *
   * @param userId 用户ID
   * @param keepCount 保留的会话数量
   */
  @Override
  public void revokeOldUserSessions(Long userId, int keepCount) {
    String userSessionKey = authProperties.getUserSessionPrefix() + userId;
    Set<String> sessions = redisTemplate.opsForSet().members(userSessionKey);
    
    if (sessions != null && sessions.size() > keepCount) {
      // 简单实现：删除所有会话，只保留最新的
      // 在实际应用中，可以根据Token的创建时间来判断新旧
      int revokeCount = sessions.size() - keepCount;
      int count = 0;
      
      for (String token : sessions) {
        if (count >= revokeCount) {
          break;
        }
        revokeToken(token);
        redisTemplate.opsForSet().remove(userSessionKey, token);
        count++;
      }
      
      log.info("已撤销用户 {} 的 {} 个旧会话，保留 {} 个", userId, revokeCount, keepCount);
    }
  }

}
